package hypeerweb;

import communications.Command;
import communications.PeerCommunicator;

/**
 * The broadcast visitor broadcasts messages to all nodes in the hypeerweb. It
 * operates by using a spanning tree of the hypeerweb where every node is a
 * child of its neighbor that has the most significant one bit of its webid set
 * to 0, for example 0101 has a parent of 0001, 00110 has parent of 00010 0 has
 * no parent.
 * 
 * A message is broadcast from a start node. This node can by any node in the
 * hypeerweb. There is a path from the start node to node 0. This path is made
 * of the start node and all of the ancestors of start node including node 0.
 * This path is called the spine. The spine when the start node is 011010 would
 * be 011010, 001010, 000010, 000000.
 * 
 * The only time a child sends a message to a parent is when the child is in the
 * spine. A parent doesn't send a message to a child is the child is in the
 * spine. This is because the parent would have received the message from the
 * child so the parent shouldn't send the message to that child
 * 
 * @author James
 * 
 */
public abstract class BroadcastVisitor implements Visitor {
	// TODO make this and all subclasses of BroadcastVisitor serializable

	protected static final String STARTED_KEY = "BroadcastVisitorStarted";

	public BroadcastVisitor() {

	}

	public static Parameters createInitialParameters(WebId startNodeId) {
		Parameters parameters = new Parameters();

		parameters.set(STARTED_KEY, startNodeId);

		return parameters;
	}

	protected abstract void operation(Node node, Parameters parameters);

	/**
	 * A node is considered in the spine of a spanning tree if it is an ancestor
	 * of the startNode or is the startNode
	 * 
	 * @param testNodeId
	 *            the node to check if it is in the spine
	 * @param startNodeId
	 *            the node the message was broadcast from
	 * @return returns true of the testNodeId is an ancestor or the same as
	 *         startNodeId
	 */
	private static boolean isInSpine(int testNodeId, int startNodeId) {
		int mask = Integer.MAX_VALUE;

		while (mask != 0) {
			if ((startNodeId & mask) == testNodeId)
				return true;

			mask >>= 1;
		}

		return false;
	}

	/**
	 * 
	 * @param webid
	 *            the webid to get the parent of
	 * @return returns the int value of the webid with the most significat one
	 *         bit in the value of webid switcthed to zero
	 */
	private static int getWebIdParent(int webid) {
		int mask = Integer.MAX_VALUE;

		while (mask != 0) {
			mask >>= 1;

			if ((webid & mask) != webid)
				return webid & mask;
		}

		return webid;
	}

	/**
	 * Visits the parent of the given node. This method should only be called if
	 * the child is in the spine
	 * 
	 * @param node
	 * @param parameters
	 */
	private void visitParent(Node node, Parameters parameters) {
		int parentId = getWebIdParent(node.getWebIdValue());

		for (Node neighbor : node.getNeighbors()) {
			if (neighbor.getWebIdValue() == parentId) {
				neighbor.accept(this, parameters);
				break;
			}
		}
	}

	/**
	 * Visits all of the children of a given node, if one of the children is in
	 * the spine that child is skipped.
	 * 
	 * @param node
	 * @param parameters
	 * @param startNodeId
	 */
	private void visitChildren(Node node, Parameters parameters,
			int startNodeId) {
		
		for (Node neighbor : node.getNeighbors()) {
			boolean isNeighborChild = getWebIdParent(neighbor.getWebIdValue()) == node.getWebIdValue();
			boolean isChildInSpine = isInSpine(neighbor.getWebIdValue(), startNodeId);

			if (isNeighborChild && !isChildInSpine) {
				neighbor.accept(this, parameters);
			}
		}
	}

    @Override
	public void visit(Node node, Parameters parameters) {
		this.operation(node, parameters);

		WebId startNodeId = (WebId) parameters.get(STARTED_KEY);

		if (isInSpine(node.getWebIdValue(), startNodeId.getValue()))
			visitParent(node, parameters);

		visitChildren(node, parameters, startNodeId.getValue());
	}
    
    @Override
	public void visit(NodeProxy node, Parameters parameters)
	{
        String[] parameterTypeNames = new String[2];
        parameterTypeNames[0] = "hypeerweb.Visitor";
        parameterTypeNames[1] = "hypeerweb.Parameters";
        Object[] actualParameters = new Object[2];
        actualParameters[0] = this;
        actualParameters[1] = parameters;
        Command command = new Command(node.getLocalObjectId(), "hypeerweb.Node", "accept", parameterTypeNames, actualParameters, false);
        PeerCommunicator.getSingleton().sendASynchronous(node.getGlobalObjectId(), command);
	}
}
